package dryven.request.controller.paramtransform;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import dryven.request.controller.ControllerDescription;
import dryven.request.controller.RedirectException;
import dryven.request.http.Request;
import dryven.request.http.Response;

public class CompositeActionParameterTransformer implements ActionParameterTransformer {
	
	private Iterable<ActionParameterTransformCandidate> _transformers;
	private Iterable<ActionParameterBindCandidate> _binders;
	private Iterable<ActionParameterFillCandidate> _fillers;
	
	private enum ParameterConsuption {
		/* this method argument never requires a url parameter, 
		 * the value is generated without depending on the URL */
		Never, 
		/* this method argument can be created by a filler, but
		 * if a url parameter is available, the parameter value can also
		 * be transformed by a binder */
		WhenProvided, 
		/* this method argument always requires a url parameter,
		 * if a parameter is not provided in the url, an exception
		 * will be thrown. */
		Always
	};
	
	public CompositeActionParameterTransformer(
			Iterable<ActionParameterTransformCandidate> transformers,
			Iterable<ActionParameterBindCandidate> binders,
			Iterable<ActionParameterFillCandidate> fillers) {
		super();
		_transformers = transformers;
		_binders = binders;
		_fillers = fillers;
	}
	
	public CompositeActionParameterTransformer() {
		super();
	}
	
	public void setTransformers(Iterable<ActionParameterTransformCandidate> transformers,
			Iterable<ActionParameterBindCandidate> binders,
			Iterable<ActionParameterFillCandidate> fillers) {
		_transformers = transformers;
		_binders = binders;
		_fillers = fillers;
	}


	/* (non-Javadoc)
	 * @see dryven.request.controller.paramtransform.ActionParameterTransformer#transform(java.lang.reflect.Method, java.lang.String[])
	 */
	public ActionParameterTransformResult transform(Method m, Object[] urlParams, ActionParameterTransformResult result, Request request, Response response) {
		//align parameters
		ParameterConsuption[] requiredFields = calculateRequiredParameters(m);
		
		short requiredParameterCount = 0;
		short optionalParameterCount = 0;
		for (ParameterConsuption required : requiredFields) {
			if(required==ParameterConsuption.Always) {
				++requiredParameterCount;
			} else {
				++optionalParameterCount;
			}
		}
		
		if(requiredParameterCount>urlParams.length) {
			throw new ActionParameterTransformationException("All parameters got bound, but there where to few url parameters for this binding to continue");
		}
		
		Object[] transformedParams = fillParameters(m, urlParams,
				requiredFields, requiredParameterCount, request, response);
		
		result.setParameters(transformedParams);
		result.setTotalOptionalParameterCount(optionalParameterCount);
		result.setOptionalFilledParameterCount(urlParams.length-requiredParameterCount);
		return result;
	}


	private Object[] fillParameters(Method m, Object[] urlParams,
			ParameterConsuption[] requiredFields, short requiredParameterCount, Request request, Response response) {
		Class<?>[] paramTypes = m.getParameterTypes();
		Annotation[][] paramsAnnos = m.getParameterAnnotations();
		
		short parameterExcess = (short) (urlParams.length-requiredParameterCount);
		short urlParamCounter = 0;
		Object[] transformedParams = new Object[m.getParameterTypes().length];
		
		Feedback feedback = new Feedback();
		
		for(int i=0;i<paramTypes.length;++i) {
			Class<?> paramType = paramTypes[i];
			Annotation[] paramAnnos = paramsAnnos[i];
			feedback.reset();
			
			if(requiredFields[i]==ParameterConsuption.Always || (requiredFields[i]==ParameterConsuption.WhenProvided && parameterExcess!=0)) {
				transformedParams[i] = urlParams[urlParamCounter];
				++urlParamCounter;
				if(requiredFields[i]==ParameterConsuption.WhenProvided) {
					--parameterExcess;
				}
			}
			//apply transformers
			if(transformedParams[i]!=null) {
				for (ActionParameterTransformCandidate transformer : _transformers) {
					transformedParams[i] = transformer.apply(paramType, paramAnnos, transformedParams[i], request);
				}
			}
			//apply binders
			for (ActionParameterBindCandidate binder : _binders) {
				if(	binder.canApply(paramType, paramAnnos) && 
					(
						(transformedParams[i]==null && binder.isValueGenerator()) || 
						(transformedParams[i]!=null)
					)
				) {
					binder.apply(paramType, paramAnnos, transformedParams[i], request, response, feedback);
					if(feedback.isSet()) {
						transformedParams[i] = feedback.getValue();
						break;
					}
				}
			}
			//if still null, apply fillers
			if(!feedback.isSet()) {
				for (ActionParameterFillCandidate filler : _fillers) {
					if(filler.canApply(paramType, paramAnnos)) {
						filler.apply(paramType, paramAnnos, request, feedback);
						if(feedback.isSet()) {
							transformedParams[i] = feedback.getValue();
							break;
						}
					}
				}
			}
			
			//we will have to keep an array with what values still aren't set, null or a real value
			
			//if still null, error
			if(!feedback.isSet()) {
				throw new ActionParameterTransformationException(String.format("Invalid value \"%s\" for parameter %d of type %s in method %s.%s, no binder or filler was found to transform this value", 
						transformedParams[i],
						i,
						paramType.getName(),
						m.getDeclaringClass().getName(),
						m.getName()
					));
			}
		}
		return transformedParams;
	}


	private ParameterConsuption[] calculateRequiredParameters(Method m) {
		ParameterConsuption[] requiredFields = new ParameterConsuption[m.getParameterTypes().length];
		Class<?>[] paramTypes = m.getParameterTypes();
		Annotation[][] paramsAnnos = m.getParameterAnnotations();
		for(int i=0;i<paramTypes.length;++i) {
			Class<?> paramType = paramTypes[i];
			Annotation[] paramAnnos = paramsAnnos[i];
			
			boolean found = false;
			for (ActionParameterBindCandidate binder : _binders) {
				if(binder.canApply(paramType, paramAnnos)) {
					requiredFields[i] = binder.isValueGenerator()?ParameterConsuption.Never:ParameterConsuption.Always;
					found = true;
					break;
				}
			}
			
			if(requiredFields[i]==ParameterConsuption.Always) {
				for (ActionParameterFillCandidate filler : _fillers) {
					if(filler.canApply(paramType, paramAnnos)) {
						requiredFields[i] = ParameterConsuption.WhenProvided;
						break;
					}
				}
			}
			
			if(!found) {
				throw new ActionParameterTransformationException(String.format("Could not bind parameter %d of type %s in method %s.%s", 
						i,
						paramType.getName(),
						m.getDeclaringClass().getName(),
						m.getName()
					));
			}
		}
		return requiredFields;
	}

	@Override
	public ActionParameterSerializer[] getParameterSerializers(Method m) {
		ActionParameterSerializer[] serializers = new ActionParameterSerializer[m.getParameterTypes().length];
		Class<?>[] paramTypes = m.getParameterTypes();
		Annotation[][] paramsAnnos = m.getParameterAnnotations();
		for(int i=0;i<paramTypes.length;++i) {
			Class<?> paramType = paramTypes[i];
			Annotation[] paramAnnos = paramsAnnos[i];
			
			boolean found = false;
			for (ActionParameterBindCandidate binder : _binders) {
				if(binder.canApply(paramType, paramAnnos)) {
					if(!binder.isValueGenerator()) {
						serializers[i] = binder;
					}
					found = true;
					break;
				}
			}
			
			if(serializers[i]!=null) {
				for (ActionParameterFillCandidate filler : _fillers) {
					if(filler.canApply(paramType, paramAnnos)) {
						serializers[i] = null;
						break;
					}
				}
			}
			
			if(!found) {
				throw new ActionParameterTransformationException(String.format("Could not bind parameter %d of type %s in method %s.%s", 
						i,
						paramType.getName(),
						m.getDeclaringClass().getName(),
						m.getName()
					));
			}
		}
		return serializers;
	}
}

class Feedback implements ActionParameterTransformFeedback {

	private boolean _isSet = false;
	private Object _value;
	private ControllerDescription _redirect;
	private Map<String, Object> _cache;
	
	@Override
	public void setParameter(Object value) {
		_isSet = true;
		_value = value;
	}

	public boolean isSet() {
		return _isSet;
	}

	public Object getValue() {
		return _value;
	}
	
	public void reset() {
		_isSet = false;
		_value = null;
	}

	//TODO: don't rely on exceptions to redirect
	@Override
	public void redirect(ControllerDescription cd) {
		throw new RedirectException(cd);
		//_redirect = cd;
	}

	public ControllerDescription getRedirect() {
		return _redirect;
	}

	@Override
	public void addCacheValue(String name, Object value) {
		if(_cache==null) {
			_cache = new HashMap<String, Object>();
		}
		_cache.put(name, value);
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> T getCacheValue(String name, Class<T> type) {
		if(_cache==null) {
			return null;
		}
		Object o = _cache.get(type);
		if(o==null) {
			return null;
		}
		if(!type.isAssignableFrom(o.getClass())) {
			throw new RuntimeException("Cant cast");
		}
		return (T)o;
	}
	
}
